/**
 * ...
 * @author Franco Ponticelli
 */

package uform.model.core;
import thx.collections.HashList;
import uform.model.cell.ArrayCell;
import uform.model.cell.ValueCell;
import uform.ui.comp.Component;

using Types;
using thx.util.Results;
import uform.model.core.Crawler;
import thx.util.Message;
import Types;
import thx.error.Error;
import uform.model.value.ValueType;
import uform.model.cell.Cell;
import thx.util.Result;
using Arrays;
using Iterators;

class Schema extends ValueType<Dynamic>
{
	static var _reOpen  = ~/^\s*\{\s*/m;
	static var _reClose = ~/^\s*\}\s*/m;
	static var _reNext  = ~/^\s*,\s*/m;
	static var _reKey   = ~/^\s*([a-z_]+[a-z0-9_]*)\s*:\s*/im;
	
	public var allowExtraFields : Bool;
	public var emptyFieldsShouldBeDeclared : Bool;
	
	var _fields : HashList<Field<Dynamic, Dynamic>>;
	public function new(allowExtraFields = true, emptyFieldsShouldBeDeclared = false)
	{
		super();
		_fields = new HashList();
		this.allowExtraFields = allowExtraFields;
		this.emptyFieldsShouldBeDeclared = emptyFieldsShouldBeDeclared;
	}
	
	public function addField(field : Field<Dynamic, Dynamic>)
	{
		_fields.set(field.name, field);
		return this;
	}
	
	public function addCell(name : String, cell : Cell<Dynamic, Dynamic>, ?title : String, ?defaultValue : Dynamic, ?description : String)
	{
		addField(new Field(name, cell, title, defaultValue, description));
		return this;
	}
	
	public function add(name : String, type : ValueType<Dynamic>, ?title : String, ?defaultValue : Dynamic, ?description : String)
	{
		addCell(name, new ValueCell(type, false), title, defaultValue, description);
		return this;
	}
	
	public function addOpt(name : String, type : ValueType<Dynamic>, ?title : String, ?defaultValue : Dynamic, ?description : String)
	{
		addCell(name, new ValueCell(type, true), title, defaultValue, description);
		return this;
	}
	
	public function addMulti(name : String, type : ValueType<Dynamic>, min = 0, max : Null<Int> = null, ?title : String, ?defaultValue : Dynamic, ?description : String)
	{
		addCell(name, new ArrayCell(type, min, max), title, defaultValue, description);
		return this;
	}
	
	
	public function get(name : String)
	{
		return _fields.get(name);
	}
	
	public function remove(name : String)
	{
		return _fields.remove(name);
	}
	
	public function fields()
	{
		return _fields.iterator();
	}
	
	override function toString()
	{
		var f = [];
		for (field in _fields)
			f.push(field.toString());
		return this.className() + " { " + f.join(", ") + " }";
	}
	
// VALUE METHODS
	override function validateType(value : Dynamic) : Result
	{
		if(Reflect.isObject(value))
			return Result.Ok;
		else
			return "expected object but was {0}".failure(Types.typeName(value));
	}
	
	override function validate(value : Dynamic) : Result
	{
		if (null == value)
		{
			return "null value".failure();
		}
		var r = super.validate(value);
		switch(r)
		{
			case Ok:
				var vfields = Reflect.fields(value);
				var efields = _fields.keys().array();
				for (field in vfields)
				{
					if (efields.remove(field))
					{
						var f = get(field);
						var tcell = f.cell;
						var v = Reflect.field(value, field);
						if (null == v && f.defaultValue != null)
							v = f.defaultValue;
						switch(tcell.validate(v))
						{
							case Ok:
								continue;
							case Failure(e):
								return Result.Failure(e.add(new Message("for field '{0}'", field)));
						}
					} else if (!allowExtraFields) {
						return "the object contains the extra field '{0}' that is not included in the schema".failure(field);
					}
				}
				if (emptyFieldsShouldBeDeclared && efields.length > 0)
				{
					var v = "'" + efields.join("', '") + "'";
					return "schema expects the following fields to be declared even if they are null or empty: {0}".failure(v);
				} else if (efields.length > 0) {
					var results = [];
					for (field in efields)
					{
						var tcell = get(field).cell;
						if (!tcell.isOptional())
							results.push(field);
					}
					if (results.length == 0)
					{
						return Result.Ok;
					} else {
						var v = "'" + results.join("', '") + "'";
						return "the following fields cannot be null or empty: {0}".failure(v);
					}
				} else {
					return Result.Ok;
				}
			default:
				return r;
		}
	}
	
	override function parse(a : String, ?ref : { s : String }) : Dynamic
	{
		if (null == ref)
			ref = { s : a };

		if (!_reOpen.match(ref.s))
			throw new Error("expected object opening '{' but found '{0:T,20}'", ref.s);
		ref.s = _reOpen.matchedRight();
		
		var result = {};
		
		var next = !_reClose.match(ref.s);
		while (next)
		{
			if (!_reKey.match(ref.s))
				throw new Error("expected field key but was '{0}'", ref.s);

			var key = _reKey.matched(1);
			ref.s = _reKey.matchedRight();
			
			var cell = get(key).cell;
			var value = cell.parse(ref.s, ref);
			
			Reflect.setField(result, key, value);
			
			if (next = _reNext.match(ref.s))
				ref.s = _reNext.matchedRight();
		}
		
		if (!_reClose.match(ref.s))
			throw new Error("expected object closing '}' but found '{0:T,20}'", ref.s);
		ref.s = _reClose.matchedRight();
			
		return result;
	}

	override function render(value : Dynamic) : String
	{
		if (!Types.isAnonymous(value))
			return null;
		var buf = [];
		for (key in _fields.keys())
		{
			var cell = get(key).cell;
			buf.push(key + ":" + cell.render(Reflect.field(value, key)));
		}
		return "{" + buf.join(",") + "}";
	}
	
	static var _reSegment = ~/^([a-z0-9_]+)/i;
	override public function resolve(crawler : Crawler)
	{
		var key = crawler.next();
		if (!_reSegment.match(key))
			throw new Error("unable to crawl at path {0}", key);
		key = _reSegment.matched(1);
		var cell = get(key).cell;
		if (null == cell)
			throw new Error("schema does not contain a definition for '{0}'", key);
			
		key = _reSegment.matched(1);
		var rest = _reSegment.matchedRight();
		
		var pget = crawler.get;
		crawler.get = function()
		{
			var t : Dynamic = pget();
			if (null == t)
				return null;
			else
				return Reflect.field(t, key);
		};
		
		var pset = crawler.set;
		crawler.set = function(v)
		{
			var t : Dynamic = pget();
			if (null == t)
			{
				t = { };
				pset(t);
			}
			Reflect.setField(t, key, v);
		};
		
		crawler.handler = cell;
		cell.resolve(crawler, rest);
		
		if (crawler.hasNext())
		{
			if (!Std.is(cell.type, Schema))
				throw new Error("current cell is not of an object type '{0}'", crawler.current);
			var cschema : Schema = cast cell.type;
			cschema.resolve(crawler);
		}
	}
}